// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/clone.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/type_environment.dart';

import '../../js_interop_checks.dart' show JsInteropChecks;
import '../js_interop.dart'
    show
        getJSName,
        hasAnonymousAnnotation,
        hasDartJSInteropAnnotation,
        hasJSInteropAnnotation,
        hasNativeAnnotation,
        hasStaticInteropAnnotation,
        hasTrustTypesAnnotation;

/// Function type that given an [Expression], which is an invocation of a static
/// interop member, and the list of [Arguments] to that invocation, returns an
/// [Expression] that inlines the static interop member call.
///
/// In order to avoid recomputing information about the same static interop
/// member, we utilize closures that contain a lot of that information already.
/// We compute one [_InvocationBuilder] per node, and reuse as needed for
/// multiple invocations of the same static interop member.
typedef _InvocationBuilder =
    Expression Function(Arguments arguments, Expression invocation);

/// Replaces js_util methods with inline calls to foreign_helper JS which
/// emits the code as a JavaScript code fragment.
class JsUtilOptimizer extends Transformer {
  final Procedure _allowInteropTarget;
  final Iterable<Procedure> _allowedInteropJsUtilTargets;
  final Procedure _callMethodTarget;
  final Procedure _callMethodTrustTypeTarget;
  final List<Procedure> _callMethodUncheckedTargets;
  final List<Procedure> _callMethodUncheckedTrustTypeTargets;
  final Procedure _callConstructorTarget;
  final List<Procedure> _callConstructorUncheckedTargets;
  final Procedure _functionToJSTarget;
  final Procedure _functionToJSCaptureThisTarget;
  final List<Procedure> _functionToJSTargets;
  final List<Procedure>? _functionToJSCaptureThisTargets;
  final Procedure _functionToJSNTarget;
  final Procedure _functionToJSCaptureThisNTarget;
  final Procedure _getPropertyTarget;
  final Procedure _getPropertyTrustTypeTarget;
  final Procedure _globalContextTarget;
  final Procedure _jsExportedDartFunctionToDartTarget;
  final Procedure _jsFunctionToDart;
  final Procedure _jsTarget;
  final Procedure _listEmptyFactory;
  final InterfaceType _objectType;
  final Procedure _setPropertyTarget;
  final Procedure _setPropertyUncheckedTarget;

  final CoreTypes _coreTypes;
  final CloneVisitorNotMembers _cloner = CloneVisitorWithMembers();
  final ExtensionIndex _extensionIndex;
  final Map<Member, _InvocationBuilder?> _externalInvocationBuilders = {};
  final StatefulStaticTypeContext _staticTypeContext;

  final bool isDart2JS;

  /// Dynamic members in js_util that interop allowed.
  static const List<String> _allowedInteropJsUtilMembers = [
    'callConstructor',
    'callMethod',
    'getProperty',
    'jsify',
    'newObject',
    'setProperty',
  ];

  JsUtilOptimizer(
    this._coreTypes,
    ClassHierarchy hierarchy,
    this._extensionIndex, {
    required this.isDart2JS,
  }) : _callMethodTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         'callMethod',
       ),
       _callMethodTrustTypeTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         '_callMethodTrustType',
       ),
       _callMethodUncheckedTargets = List<Procedure>.generate(
         5,
         (i) => _coreTypes.index.getTopLevelProcedure(
           'dart:js_util',
           '_callMethodUnchecked$i',
         ),
       ),
       _callMethodUncheckedTrustTypeTargets = List<Procedure>.generate(
         5,
         (i) => _coreTypes.index.getTopLevelProcedure(
           'dart:js_util',
           '_callMethodUncheckedTrustType$i',
         ),
       ),
       _callConstructorTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         'callConstructor',
       ),
       _callConstructorUncheckedTargets = List<Procedure>.generate(
         5,
         (i) => _coreTypes.index.getTopLevelProcedure(
           'dart:js_util',
           '_callConstructorUnchecked$i',
         ),
       ),
       _functionToJSTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_interop',
         'FunctionToJSExportedDartFunction|get#toJS',
       ),
       _functionToJSCaptureThisTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_interop',
         'FunctionToJSExportedDartFunction|get#toJSCaptureThis',
       ),
       _functionToJSTargets = List<Procedure>.generate(
         6,
         (i) => _coreTypes.index.getTopLevelProcedure(
           'dart:js_util',
           '_functionToJS$i',
         ),
       ),
       _functionToJSCaptureThisTargets =
           isDart2JS
               ? List<Procedure>.generate(
                 5,
                 (i) => _coreTypes.index.getTopLevelProcedure(
                   'dart:js_util',
                   '_functionToJSCaptureThis$i',
                 ),
               )
               : null,
       _functionToJSNTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         '_functionToJSN',
       ),
       _functionToJSCaptureThisNTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         '_functionToJSCaptureThisN',
       ),
       _getPropertyTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         'getProperty',
       ),
       _getPropertyTrustTypeTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         '_getPropertyTrustType',
       ),
       _globalContextTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:_js_helper',
         'get:staticInteropGlobalContext',
       ),
       _jsExportedDartFunctionToDartTarget = _coreTypes.index
           .getTopLevelProcedure(
             'dart:js_interop',
             'JSExportedDartFunctionToFunction|get#toDart',
           ),
       _jsFunctionToDart = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         '_jsFunctionToDart',
       ),
       _objectType = hierarchy.coreTypes.objectNonNullableRawType,
       _setPropertyTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         'setProperty',
       ),
       _setPropertyUncheckedTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         '_setPropertyUnchecked',
       ),
       _jsTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:_foreign_helper',
         'JS',
       ),
       _allowInteropTarget = _coreTypes.index.getTopLevelProcedure(
         'dart:js_util',
         'allowInterop',
       ),
       _allowedInteropJsUtilTargets = _allowedInteropJsUtilMembers.map(
         (member) =>
             _coreTypes.index.getTopLevelProcedure('dart:js_util', member),
       ),
       _listEmptyFactory = _coreTypes.index.getProcedure(
         'dart:core',
         'List',
         'empty',
       ),
       _staticTypeContext = StatefulStaticTypeContext.stacked(
         TypeEnvironment(_coreTypes, hierarchy),
       );

  @override
  TreeNode visitLibrary(Library node) {
    _staticTypeContext.enterLibrary(node);
    node.transformChildren(this);
    _staticTypeContext.leaveLibrary(node);
    return node;
  }

  @override
  TreeNode defaultMember(Member node) {
    _staticTypeContext.enterMember(node);
    node.transformChildren(this);
    _staticTypeContext.leaveMember(node);
    return node;
  }

  @override
  TreeNode visitProcedure(Procedure node) {
    if (!isDart2JS) return defaultMember(node);

    if (node.isExternal &&
        node.isStatic &&
        !JsInteropChecks.isPatchedMember(node)) {
      if (node.kind == ProcedureKind.Getter) {
        final dottedPrefix = _getDottedPrefixForStaticallyResolvableMember(
          node,
        );
        if (dottedPrefix != null) {
          // Convert the external getter into a Dart getter with a body of the
          // form
          //
          //    => getPropertyTrustType<dynamic>(
          //           staticInteropGlobalContext(),
          //           "name"
          //         ) as <type>;
          //
          // The type argument is `<dynamic>` so the downcast can be removed
          // under `-O3`.

          final receiver = _getObjectOffGlobalContext(
            node,
            dottedPrefix.isEmpty ? [] : dottedPrefix.split('.'),
          );

          final shouldTrustType =
              node.enclosingClass != null &&
              hasTrustTypesAnnotation(node.enclosingClass!);

          Expression expression = StaticInvocation(
            _getPropertyTrustTypeTarget,
            Arguments(
              [receiver, StringLiteral(_getMemberJSName(node))],
              types: [DynamicType()],
            ),
          )..fileOffset = node.fileOffset;

          if (!shouldTrustType) {
            expression =
                AsExpression(expression, node.getterType)
                  ..isTypeError = true
                  ..isForDynamic = true
                  ..fileOffset = node.fileOffset;
          }

          final statement = ReturnStatement(expression)
            ..fileOffset = node.fileOffset;

          node.function.body = statement;
          node.isExternal = false;
        } else {
          // Leave the static getter member as `external`.
        }
      }
    }

    return defaultMember(node);
  }

  /// Given a static interop procedure [node], return a
  /// [_InvocationBuilder] that will create new [StaticInvocation]s that
  /// replace calls to [node].
  ///
  /// If [node] is not one of several static interop members, this function
  /// returns null.
  _InvocationBuilder? _getExternalInvocationBuilder(Procedure node) {
    if (node.isExternal) {
      if (_extensionIndex.isInstanceInteropMember(node)) {
        var shouldTrustType = _extensionIndex.isTrustTypesMember(node);
        if (_extensionIndex.isGetter(node)) {
          return _getExternalGetterInvocationBuilder(node, shouldTrustType);
        } else if (_extensionIndex.isSetter(node)) {
          return _getExternalSetterInvocationBuilder(node);
        } else if (_extensionIndex.isMethod(node)) {
          return _getExternalMethodInvocationBuilder(node, shouldTrustType);
        } else if (_extensionIndex.isOperator(node)) {
          return _getExternalOperatorInvocationBuilder(node, shouldTrustType);
        }
      } else {
        // Do the lowerings for top-levels, static class members, and
        // constructors/factories.
        var dottedPrefix = _getDottedPrefixForStaticallyResolvableMember(node);
        if (dottedPrefix != null) {
          var receiver = _getObjectOffGlobalContext(
            node,
            dottedPrefix.isEmpty ? [] : dottedPrefix.split('.'),
          );
          var shouldTrustType =
              node.enclosingClass != null &&
              hasTrustTypesAnnotation(node.enclosingClass!);
          if (_extensionIndex.isGetter(node)) {
            if (isDart2JS) {
              // Top-level and static external getters are given a Dart body so
              // that annotations can remain attached. The call to the getter
              // remains in place. The dart2js optimizer may do inlining.
              return null;
            }
            // For DDC, the call to the getter is replaced with the lowered code
            // in this transformer.
            // TODO(http://dartbug.com/60505): Consider also using a synthesized
            // getter and leaving the call in place.
            return _getExternalGetterInvocationBuilder(
              node,
              shouldTrustType,
              receiver,
            );
          } else if (_extensionIndex.isSetter(node)) {
            return _getExternalSetterInvocationBuilder(node, receiver);
          } else if (_extensionIndex.isMethod(node)) {
            return _getExternalMethodInvocationBuilder(
              node,
              shouldTrustType,
              receiver,
            );
          } else if (_extensionIndex.isNonLiteralConstructor(node)) {
            // Get the constructor object using the class name.
            return _getExternalConstructorInvocationBuilder(
              node,
              _getObjectOffGlobalContext(node, dottedPrefix.split('.')),
            );
          }
        }
      }
    }
    return null;
  }

  /// Returns the prefixed JS name for the given [node] using the enclosing
  /// library's, enclosing class' (if any), and member's `@JS` values.
  ///
  /// Returns null if [node] is not external and one of:
  /// 1. A top-level member
  /// 2. A `@staticInterop` factory
  /// 3. A `@staticInterop` static member
  /// 4. A `@JS` extension type constructor
  /// 5. A `@JS` extension type static member
  String? _getDottedPrefixForStaticallyResolvableMember(Procedure node) {
    if (!node.isExternal || node.isExtensionMember) return null;

    var dottedPrefix = getJSName(node.enclosingLibrary);

    if (!node.isExtensionTypeMember &&
        node.enclosingClass == null &&
        (hasDartJSInteropAnnotation(node) ||
            hasDartJSInteropAnnotation(node.enclosingLibrary))) {
      // If the `@JS` value of the node has any '.'s, we take the entries
      // before the last '.' to determine the dotted prefix name.
      var jsName = getJSName(node);
      if (jsName.isNotEmpty) {
        var lastDotIndex = jsName.lastIndexOf('.');
        if (lastDotIndex != -1) {
          dottedPrefix = concatenateJSNames(
            dottedPrefix,
            jsName.substring(0, lastDotIndex),
          );
        }
      }
    } else {
      Annotatable enclosingClass;
      if (node.isExtensionTypeMember) {
        var descriptor = _extensionIndex.getExtensionTypeDescriptor(node);
        if (descriptor == null ||
            (!descriptor.isStatic &&
                descriptor.kind != ExtensionTypeMemberKind.Constructor &&
                descriptor.kind != ExtensionTypeMemberKind.Factory)) {
          return null;
        }
        enclosingClass = _extensionIndex.getExtensionType(node)!;
      } else if (node.enclosingClass != null &&
          hasStaticInteropAnnotation(node.enclosingClass!)) {
        if (!node.isFactory && !node.isStatic) return null;
        enclosingClass = node.enclosingClass!;
      } else {
        return null;
      }
      // `@staticInterop` or `@JS` extension type
      // factory/constructor/static member, use the class name as part of the
      // dotted prefix.
      var className = getJSName(enclosingClass);
      if (className.isEmpty) {
        className =
            enclosingClass is Class
                ? enclosingClass.name
                : (enclosingClass as ExtensionTypeDeclaration).name;
      }
      dottedPrefix = concatenateJSNames(dottedPrefix, className);
    }
    return dottedPrefix;
  }

  // TODO(srujzs): It feels weird to have this be public, but it's weirder to
  // have this method elsewhere for now. Figure out a better place for this.
  /// Given two `@JS` values, combines them into a concatenated name using '.'.
  ///
  /// If either parameters are empty, returns the other.
  static String concatenateJSNames(String prefix, String suffix) {
    if (prefix.isEmpty) return suffix;
    if (suffix.isEmpty) return prefix;
    return '$prefix.$suffix';
  }

  /// Given a list of strings, [selectors], recursively fetches the property
  /// that corresponds to each string off of the global context.
  ///
  /// Returns an expression that contains the nested property gets.
  Expression _getObjectOffGlobalContext(
    Procedure node,
    List<String> selectors,
  ) {
    Expression currentTarget = StaticGet(_globalContextTarget);
    for (String selector in selectors) {
      currentTarget = StaticInvocation(
        _getPropertyTrustTypeTarget,
        Arguments(
          [currentTarget, StringLiteral(selector)],
          types: [_objectType],
        ),
      );
    }
    return currentTarget;
  }

  /// Returns a new [_InvocationBuilder] for the given [node] external
  /// getter.
  ///
  /// The builder will return an [Expression] that will call the optimized
  /// version of `js_util.getProperty` for the given external getter. If
  /// [shouldTrustType] is true, the builder creates a variant that does not
  /// check the return type. If [maybeReceiver] is non-null, the builder uses
  /// that instead of the first positional argument as the receiver for
  /// `js_util.getProperty`.
  _InvocationBuilder _getExternalGetterInvocationBuilder(
    Procedure node,
    bool shouldTrustType, [
    Expression? maybeReceiver,
  ]) {
    final target =
        shouldTrustType ? _getPropertyTrustTypeTarget : _getPropertyTarget;
    final isInstanceInteropMember = _extensionIndex.isInstanceInteropMember(
      node,
    );
    final name = _getMemberJSName(node);
    return (Arguments arguments, Expression invocation) {
      // Parameter `this` only exists for extension and extension type instance
      // members.
      final positionalArgs = arguments.positional;
      assert(positionalArgs.length == (isInstanceInteropMember ? 1 : 0));
      // We clone the receiver as each invocation needs a fresh node.
      final receiver =
          maybeReceiver == null
              ? positionalArgs.first
              : _cloner.clone(maybeReceiver);
      final property = StringLiteral(name);
      return StaticInvocation(
          target,
          Arguments(
            [receiver, property],
            types: [invocation.getStaticType(_staticTypeContext)],
          ),
        )
        ..fileOffset = invocation.fileOffset
        ..parent = invocation.parent;
    };
  }

  /// Returns a new [_InvocationBuilder] for the given [node] external
  /// setter.
  ///
  /// The builder will return an [Expression] that will call the optimized
  /// version of `js_util.setProperty` for the given external setter. If
  /// [maybeReceiver] is non-null, the builder uses that instead of the first
  /// positional argument as the receiver for `js_util.setProperty`.
  _InvocationBuilder _getExternalSetterInvocationBuilder(
    Procedure node, [
    Expression? maybeReceiver,
  ]) {
    final isInstanceInteropMember = _extensionIndex.isInstanceInteropMember(
      node,
    );
    final name = _getMemberJSName(node);
    return (Arguments arguments, Expression invocation) {
      // Parameter `this` only exists for extension and extension type instance
      // members.
      final positionalArgs = arguments.positional;
      assert(positionalArgs.length == (isInstanceInteropMember ? 2 : 1));
      final receiver =
          maybeReceiver == null
              ? positionalArgs.first
              : _cloner.clone(maybeReceiver);
      final property = StringLiteral(name);
      final value = positionalArgs.last;
      return _lowerSetProperty(
        StaticInvocation(
            _setPropertyTarget,
            Arguments(
              [receiver, property, value],
              types: [value.getStaticType(_staticTypeContext)],
            ),
          )
          ..fileOffset = invocation.fileOffset
          ..parent = invocation.parent,
      );
    };
  }

  /// Returns a new [_InvocationBuilder] for the given [node] external
  /// method.
  ///
  /// The builder will return an [Expression] that will call the optimized
  /// version of `js_util.callMethod` for the given external method. If
  /// [shouldTrustType] is true, the builder creates a variant that does not
  /// check the return type. If [maybeReceiver] is non-null, the builder uses
  /// that instead of the first positional argument as the receiver for
  /// `js_util.callMethod`.
  _InvocationBuilder _getExternalMethodInvocationBuilder(
    Procedure node,
    bool shouldTrustType, [
    Expression? maybeReceiver,
  ]) {
    final target =
        shouldTrustType ? _callMethodTrustTypeTarget : _callMethodTarget;
    final isInstanceInteropMember = _extensionIndex.isInstanceInteropMember(
      node,
    );
    final name = _getMemberJSName(node);
    return (Arguments arguments, Expression invocation) {
      var positional = arguments.positional;
      final receiver =
          maybeReceiver == null
              ? positional.first
              : _cloner.clone(maybeReceiver);
      if (isInstanceInteropMember) {
        // Ignore `this` for extension and extension type members.
        positional = positional.sublist(1);
      }
      final callMethodInvocation =
          StaticInvocation(
              target,
              Arguments(
                [receiver, StringLiteral(name), ListLiteral(positional)],
                types: [invocation.getStaticType(_staticTypeContext)],
              ),
            )
            ..fileOffset = invocation.fileOffset
            ..parent = invocation.parent;
      return _lowerCallMethod(
        callMethodInvocation,
        shouldTrustType: shouldTrustType,
      );
    };
  }

  /// Returns a new [_InvocationBuilder] for the [node] external operator.
  ///
  /// This function only supports '[]' and '[]=' for now.
  _InvocationBuilder? _getExternalOperatorInvocationBuilder(
    Procedure node,
    bool shouldTrustType,
  ) {
    final operator =
        _extensionIndex.getExtensionTypeDescriptor(node)?.name.text ??
        _extensionIndex.getExtensionDescriptor(node)?.name.text;

    // TODO(srujzs): Support more operators for overloading using some
    // combination of Dart-defineable operators and @JS renaming for the ones
    // that are not renameable.
    final Procedure target;
    StaticInvocation Function(StaticInvocation)? invocationOptimizer;
    switch (operator) {
      case '[]':
        target =
            shouldTrustType ? _getPropertyTrustTypeTarget : _getPropertyTarget;
        break;
      case '[]=':
        target = _setPropertyTarget;
        invocationOptimizer = _lowerSetProperty;
        break;
      default:
        throw UnimplementedError(
          'External operator $operator is unsupported for static interop. ',
        );
    }

    return (Arguments arguments, Expression invocation) {
      final replacement =
          StaticInvocation(
              target,
              Arguments(
                arguments.positional,
                types: [invocation.getStaticType(_staticTypeContext)],
              ),
            )
            ..fileOffset = invocation.fileOffset
            ..parent = invocation.parent;
      return invocationOptimizer != null
          ? invocationOptimizer(replacement)
          : replacement;
    };
  }

  /// Returns a new [_InvocationBuilder] for the given [node] external
  /// non-object literal factory.
  ///
  /// The builder will return an [Expression] that will call the optimized
  /// version of `js_util.callConstructor` using the given [constructor] and the
  /// [Arguments] of the [Expression] that calls [node].
  _InvocationBuilder _getExternalConstructorInvocationBuilder(
    Procedure node,
    Expression constructor,
  ) {
    final function = node.function;
    assert(function.namedParameters.isEmpty);
    return (Arguments arguments, Expression invocation) {
      final callConstructorInvocation =
          StaticInvocation(
              _callConstructorTarget,
              Arguments(
                [_cloner.clone(constructor), ListLiteral(arguments.positional)],
                types: [invocation.getStaticType(_staticTypeContext)],
              ),
            )
            ..fileOffset = invocation.fileOffset
            ..parent = invocation.parent;
      return _lowerCallConstructor(callConstructorInvocation);
    };
  }

  /// Returns the underlying JS name.
  ///
  /// Returns either the name from the `@JS` annotation if non-empty, or the
  /// declared name of the member. In the case of an extension or extension type
  /// member, this does not return the CFE generated name for the top level
  /// member, but rather the name of the original member.
  String _getMemberJSName(Procedure node) {
    var jsAnnotationName = getJSName(node);
    if (jsAnnotationName.isNotEmpty) {
      // In the case of top-level external members, this may contain '.'. The
      // namespacing before the last '.' should be resolved when we provide a
      // receiver to the lowerings. Here, we just take the final identifier.
      return jsAnnotationName.split('.').last;
    } else if (node.isExtensionMember) {
      return _extensionIndex.getExtensionDescriptor(node)!.name.text;
    } else if (node.isExtensionTypeMember) {
      return _extensionIndex.getExtensionTypeDescriptor(node)!.name.text;
    } else {
      return node.name.text;
    }
  }

  /// Replaces js_util method and static interop member calls with optimization
  /// when possible.
  ///
  /// - Lowers `setProperty` to  `_setPropertyUnchecked` for values that are
  /// not Function type and guaranteed to be interop allowed.
  /// - Lowers `callMethod` to `_callMethodUncheckedN` when the number of given
  /// arguments is 0-4 and all arguments are guaranteed to be interop allowed.
  /// - Lowers `callConstructor` to `_callConstructorUncheckedN` when there are
  /// 0-4 arguments and all arguments are guaranteed to be interop allowed.
  /// - Computes and caches a [_InvocationBuilder] for a given non-custom static
  /// interop invocation, and then calls that builder to replace the current
  /// [node].
  @override
  TreeNode visitStaticInvocation(StaticInvocation node) {
    Expression invocation = node;
    final target = node.target;
    if (target == _setPropertyTarget) {
      invocation = _lowerSetProperty(node);
    } else if (target == _callMethodTarget) {
      // Never trust types on explicit `js_util` calls.
      invocation = _lowerCallMethod(node, shouldTrustType: false);
    } else if (target == _callConstructorTarget) {
      invocation = _lowerCallConstructor(node);
      // TODO(srujzs): Delete the `isPatchedMember` check once
      // https://github.com/dart-lang/sdk/issues/53367 is resolved.
    } else if (target == _functionToJSTarget) {
      invocation = _lowerFunctionToJS(node);
    } else if (target == _functionToJSCaptureThisTarget) {
      invocation = _lowerFunctionToJS(node, captureThis: true);
    } else if (target == _jsExportedDartFunctionToDartTarget) {
      invocation = _lowerJSExportedDartFunctionToDart(node);
    } else if (target.isExternal && !JsInteropChecks.isPatchedMember(target)) {
      final builder = _externalInvocationBuilders.putIfAbsent(
        target,
        () => _getExternalInvocationBuilder(target),
      );
      if (builder != null) invocation = builder(node.arguments, node);
    }
    invocation.transformChildren(this);
    return invocation;
  }

  @override
  TreeNode visitStaticGet(StaticGet node) {
    Expression invocation = node;
    final target = node.target;
    if (target.isExternal && target is Procedure) {
      // Reference to a static interop getter declared as static. Note that we
      // provide no arguments as static getters do not have a 'this'.
      final builder = _externalInvocationBuilders.putIfAbsent(
        target,
        () => _getExternalInvocationBuilder(target),
      );
      if (builder != null) invocation = builder(Arguments([]), node);
    }
    invocation.transformChildren(this);
    return invocation;
  }

  @override
  TreeNode visitStaticSet(StaticSet node) {
    Expression invocation = node;
    final target = node.target;
    if (target.isExternal && target is Procedure) {
      // Reference to a static interop setter declared as static. Note that we
      // provide only the value as static setters do not have a 'this'.
      final builder = _externalInvocationBuilders.putIfAbsent(
        target,
        () => _getExternalInvocationBuilder(target),
      );
      if (builder != null) invocation = builder(Arguments([node.value]), node);
    }
    invocation.transformChildren(this);
    return invocation;
  }

  /// Lowers the given js_util `setProperty` call to `_setPropertyUnchecked`
  /// when the additional validation checks in `setProperty` can be elided.
  ///
  /// Removing the checks allows further inlining by the compilers.
  StaticInvocation _lowerSetProperty(StaticInvocation node) {
    Arguments arguments = node.arguments;
    assert(arguments.positional.length == 3);
    assert(arguments.named.isEmpty);

    if (!_allowedInterop(arguments.positional.last)) {
      return node;
    }

    return StaticInvocation(_setPropertyUncheckedTarget, arguments)
      ..fileOffset = node.fileOffset
      ..parent = node.parent;
  }

  /// Lowers the given js_util `callMethod` call to `_callMethodUncheckedN`
  /// when the additional validation checks on the arguments can be elided.
  ///
  /// Calls will be lowered when using a List literal or constant list with 0-4
  /// elements for the `callMethod` arguments, or the `List.empty()` factory.
  /// Removing the checks allows further inlining by the compilers.
  StaticInvocation _lowerCallMethod(
    StaticInvocation node, {
    required bool shouldTrustType,
  }) {
    Arguments arguments = node.arguments;
    assert(arguments.positional.length == 3);
    assert(arguments.named.isEmpty);
    List<Procedure> targets =
        shouldTrustType
            ? _callMethodUncheckedTrustTypeTargets
            : _callMethodUncheckedTargets;

    return _lowerToCallUnchecked(
      node,
      targets,
      arguments.positional.sublist(0, 2),
    );
  }

  /// Lowers the given js_util `callConstructor` call to
  /// `_callConstructorUncheckedN` when the additional validation checks on the
  /// arguments can be elided.
  ///
  /// Calls will be lowered when using a List literal or constant list with 0-4
  /// elements for the `callConstructor` arguments, or the `List.empty()`
  /// factory. Removing the checks allows further inlining by the compilers.
  StaticInvocation _lowerCallConstructor(StaticInvocation node) {
    Arguments arguments = node.arguments;
    assert(arguments.positional.length == 2);
    assert(arguments.named.isEmpty);

    return _lowerToCallUnchecked(node, _callConstructorUncheckedTargets, [
      arguments.positional.first,
    ]);
  }

  /// Helper to lower the given [node] to the relevant unchecked target in the
  /// [callUncheckedTargets] based on whether the validation checks on the
  /// [originalArguments] can be elided.
  ///
  /// Calls will be lowered when using a List literal or constant list with 0-4
  /// arguments, or the `List.empty()` factory. Removing the checks allows further
  /// inlining by the compilers.
  StaticInvocation _lowerToCallUnchecked(
    StaticInvocation node,
    List<Procedure> callUncheckedTargets,
    List<Expression> originalArguments,
  ) {
    var argumentsList = node.arguments.positional.last;
    // Lower arguments in a List.empty factory call.
    if (argumentsList is StaticInvocation &&
        argumentsList.target == _listEmptyFactory) {
      return _createCallUncheckedNode(
        callUncheckedTargets,
        node.arguments.types,
        [],
        originalArguments,
        node.fileOffset,
        node.parent,
        node.arguments.fileOffset,
      );
    }

    // Lower arguments in other kinds of Lists.
    List<Expression> callUncheckedArguments;
    DartType entryType;
    if (argumentsList is ListLiteral) {
      if (argumentsList.expressions.length >= callUncheckedTargets.length) {
        return node;
      }
      callUncheckedArguments = argumentsList.expressions;
      entryType = argumentsList.typeArgument;
    } else if (argumentsList is ConstantExpression &&
        argumentsList.constant is ListConstant) {
      var argumentsListConstant = argumentsList.constant as ListConstant;
      if (argumentsListConstant.entries.length >= callUncheckedTargets.length) {
        return node;
      }
      callUncheckedArguments =
          argumentsListConstant.entries
              .map<Expression>(
                (constant) => ConstantExpression(
                  constant,
                  constant.getType(_staticTypeContext),
                ),
              )
              .toList();
      entryType = argumentsListConstant.typeArgument;
    } else {
      // Skip lowering arguments in any other type of List.
      return node;
    }

    // Check the arguments List type, then verify each argument if needed.
    if (!_allowedInteropType(entryType)) {
      for (var argument in callUncheckedArguments) {
        if (!_allowedInterop(argument)) {
          return node;
        }
      }
    }

    return _createCallUncheckedNode(
      callUncheckedTargets,
      node.arguments.types,
      callUncheckedArguments,
      originalArguments,
      node.fileOffset,
      node.parent,
      node.arguments.fileOffset,
    );
  }

  /// Creates a new StaticInvocation node for the relevant unchecked target
  /// with the given 0-4 arguments.
  StaticInvocation _createCallUncheckedNode(
    List<Procedure> callUncheckedTargets,
    List<DartType> callUncheckedTypes,
    List<Expression> callUncheckedArguments,
    List<Expression> originalArguments,
    int nodeFileOffset,
    TreeNode? nodeParent,
    int argumentsFileOffset,
  ) {
    assert(callUncheckedArguments.length <= 4);
    return StaticInvocation(
        callUncheckedTargets[callUncheckedArguments.length],
        Arguments([
          ...originalArguments,
          ...callUncheckedArguments,
        ], types: callUncheckedTypes)..fileOffset = argumentsFileOffset,
      )
      ..fileOffset = nodeFileOffset
      ..parent = nodeParent;
  }

  /// For the given `dart:js_interop` `Function.toJS` or
  /// `Function.toJSCaptureThis` invocation [node], returns an invocation of the
  /// corresponding private stub in `js_util` with the invocation's [Function]
  /// argument and the number of positional parameters of that [Function].
  ///
  /// If [captureThis] is false, the node target is assumed to be
  /// `Function.toJS` and otherwise `Function.toJSCaptureThis`.
  ///
  /// There are specialized stubs up to a certain positional parameter length,
  /// and after that, either an invocation of `_functionToJSN` or
  /// `_functionToJSCaptureThisN` is returned.
  StaticInvocation _lowerFunctionToJS(
    StaticInvocation node, {
    bool captureThis = false,
  }) {
    // JS interop checks assert that the static type is available, and that
    // there are no named arguments or type arguments.
    final function = node.arguments.positional.single;
    final functionType =
        function.getStaticType(_staticTypeContext) as FunctionType;
    final parametersLength = functionType.positionalParameters.length;
    List<Procedure>? specializedStubs;
    int stubIndex;
    Procedure genericStub;
    if (captureThis) {
      specializedStubs = _functionToJSCaptureThisTargets;
      stubIndex = parametersLength - 1; // Account for `this`.
      genericStub = _functionToJSCaptureThisNTarget;
    } else {
      specializedStubs = _functionToJSTargets;
      stubIndex = parametersLength;
      genericStub = _functionToJSNTarget;
    }
    Procedure target;
    Arguments arguments;
    if (specializedStubs != null &&
        stubIndex >= 0 &&
        stubIndex < specializedStubs.length) {
      target = specializedStubs[stubIndex];
      arguments = Arguments([function]);
    } else {
      target = genericStub;
      arguments = Arguments([function, IntLiteral(parametersLength)]);
    }
    return StaticInvocation(
        target,
        arguments..fileOffset = node.arguments.fileOffset,
      )
      ..fileOffset = node.fileOffset
      ..parent = node.parent;
  }

  /// For the given `dart:js_interop` `JSExportedDartFunction.toDart` invocation
  /// [node], returns an invocation of `_jsFunctionToDart` with the given
  /// `JSExportedDartFunction` argument.
  StaticInvocation _lowerJSExportedDartFunctionToDart(StaticInvocation node) =>
      StaticInvocation(
          _jsFunctionToDart,
          Arguments([node.arguments.positional[0]])
            ..fileOffset = node.arguments.fileOffset,
        )
        ..fileOffset = node.fileOffset
        ..parent = node.parent;

  /// Returns whether the given [node] is guaranteed to be allowed to interop
  /// with JS.
  ///
  /// [node] is guaranteed to be allowed to interop with JS if:
  /// - It is guaranteed to not be a function or is an allowlisted type.
  /// - It is an invocation of any of the allowed methods:
  ///   - dart:_foreign_helper JS
  ///   - dart:js `allowInterop`
  ///   - dart:js_util and any of the `_allowedInteropJsUtilMembers`
  bool _allowedInterop(Expression node) {
    // TODO(rileyporter): Detect functions that have been wrapped at some point
    // with `allowInterop`
    if (node is StaticInvocation) {
      if (node.target == _allowInteropTarget) return true;
      if (node.target == _jsTarget) return true;
      if (_allowedInteropJsUtilTargets.contains(node.target)) return true;
    }

    return _allowedInteropType(node.getStaticType(_staticTypeContext));
  }

  /// Returns whether the given [type] is either a type that is guaranteed to
  /// not be a function or is an allowlisted type that can flow to JS without
  /// needing to be checked.
  bool _allowedInteropType(DartType type) {
    // Static interop types and `ExternalDartReference` are allowlisted because
    // even though types like `JSAny` and `ExternalDartReference`, which are
    // actually `Object` underneath, can actually be Dart functions, that is
    // either true if there was a deliberate cast (which is linted and
    // platform-inconsistent) or it's meant to be used as an opaque reference.
    // To avoid a slower code path when using these types, we allowlist them.
    if (_extensionIndex.isStaticInteropType(type) ||
        _extensionIndex.isExternalDartReferenceType(type)) {
      return true;
    }
    type = type.extensionTypeErasure;
    // TODO(srujzs): We should check for type parameters and dynamic.
    if (type is InterfaceType) {
      return type.classNode != _coreTypes.functionClass &&
          type.classNode != _coreTypes.objectClass;
    } else {
      // Only other DartType guaranteed to not be a function.
      return type is NullType;
    }
  }
}

/// Lazily-initialized indexes for extension and extension type interop members.
///
/// As the query APIs are called, we process the enclosing libraries of the
/// member in question if needed. We only process JS interop extension types and
/// extensions on either JS interop or @Native classes.
class ExtensionIndex {
  final Map<Reference, Reference?> _coreInteropTypeIndex = {};
  final Map<Reference, Annotatable> _extensionAnnotatableIndex = {};
  final Map<Reference, Extension> _extensionIndex = {};
  final Map<Reference, ExtensionMemberDescriptor> _extensionMemberIndex = {};
  final Map<Reference, Reference> _extensionTearOffIndex = {};
  final Map<Reference, ExtensionTypeDeclaration> _extensionTypeIndex = {};
  final Map<Reference, ExtensionTypeMemberDescriptor>
  _extensionTypeMemberIndex = {};
  final Map<Reference, Reference> _extensionTypeTearOffIndex = {};
  final Set<Reference> _externalDartReferences = {};
  final Class? _javaScriptObject;
  final Set<Library> _processedExtensionLibraries = {};
  final Set<Library> _processedExtensionTypeLibraries = {};
  final Set<Reference> _shouldTrustType = {};
  final TypeEnvironment _typeEnvironment;

  ExtensionIndex(CoreTypes coreTypes, this._typeEnvironment)
    : _javaScriptObject = coreTypes.index.tryGetClass(
        'dart:_interceptors',
        'JavaScriptObject',
      );

  /// If unprocessed, for all extension members in [library] whose on-type is a
  /// JS interop or `@Native` class, does the following:
  ///
  /// - Maps the member to its on-type in `_extensionAnnotatableIndex`.
  /// - Maps the member to its extension in `_extensionIndex`.
  /// - Maps the member to its descriptor in `_extensionMemberIndex`.
  /// - Adds the member to `_shouldTrustTypes` if the on-type has a
  /// `@trustTypes` annotation.
  /// - Maps the tear-off member to the member it tears off in
  /// `extensionTearOffIndex`.
  void _indexExtensions(Library library) {
    if (_processedExtensionLibraries.contains(library)) return;
    for (var extension in library.extensions) {
      for (var descriptor in extension.memberDescriptors) {
        var reference = descriptor.memberReference!;
        var onType = extension.onType;
        bool isInteropOnType = false;
        Annotatable? cls;
        if (onType is InterfaceType) {
          cls = onType.classNode;
          // For now, `@trustTypes` can only be used on classes and not
          // extension types.
          if (hasTrustTypesAnnotation(cls)) {
            _shouldTrustType.add(reference);
          }
          isInteropOnType =
              hasJSInteropAnnotation(cls) || hasNativeAnnotation(cls);
        } else if (onType is ExtensionType) {
          final extensionType = onType.extensionTypeDeclaration;
          cls = extensionType;
          isInteropOnType = isInteropExtensionType(extensionType);
        }
        if (!isInteropOnType) continue;

        _extensionMemberIndex[reference] = descriptor;
        _extensionAnnotatableIndex[reference] = cls!;
        _extensionIndex[reference] = extension;
        final tearOffReference = descriptor.tearOffReference;
        if (tearOffReference != null) {
          _extensionMemberIndex[tearOffReference] = descriptor;
          _extensionIndex[tearOffReference] = extension;
          _extensionAnnotatableIndex[reference] = cls;
          _extensionTearOffIndex[tearOffReference] = reference;
        }
      }
    }
    _processedExtensionLibraries.add(library);
  }

  Annotatable? getExtensionAnnotatable(Member member) {
    if (!member.isExtensionMember) return null;
    _indexExtensions(member.enclosingLibrary);
    return _extensionAnnotatableIndex[member.reference];
  }

  Extension? getExtension(Member member) {
    if (!member.isExtensionMember) return null;
    _indexExtensions(member.enclosingLibrary);
    return _extensionIndex[member.reference];
  }

  ExtensionMemberDescriptor? getExtensionDescriptor(Member member) {
    if (!member.isExtensionMember) return null;
    _indexExtensions(member.enclosingLibrary);
    return _extensionMemberIndex[member.reference];
  }

  bool isTrustTypesMember(Member member) {
    if (!member.isExtensionMember) return false;
    _indexExtensions(member.enclosingLibrary);
    return _shouldTrustType.contains(member.reference);
  }

  Reference? getExtensionMemberForTearOff(Member member) {
    if (!member.isExtensionMember) return null;
    _indexExtensions(member.enclosingLibrary);
    return _extensionTearOffIndex[member.reference];
  }

  /// Returns whether [extensionType] is an "interop extension type".
  ///
  /// Interop extension types have either another interop extension type or a
  /// "core" interop type (see below) as their representation type. Extension
  /// types can only declare external JS interop members if they are interop
  /// extension types.
  bool isInteropExtensionType(ExtensionTypeDeclaration extensionType) {
    return getCoreInteropType(
          // Nullability is irrelevant for this purpose.
          ExtensionType(extensionType, Nullability.undetermined),
        ) !=
        null;
  }

  /// Returns the "core" interop type of [type], unwrapping extension types as
  /// needed and caching along the way.
  ///
  /// A type is a "core" interop type if it is:
  /// - a `dart:js_interop` extension type
  /// - a `@staticInterop` type
  /// - an `@Native` type that <: `JavaScriptObject`. Note that this excludes
  ///   `dart:typed_data`, as typed list factories return a type that is
  ///   <: `JavaScriptObject`, but the typed lists themselves are not such a
  ///   type. This is expected and intended since unlike `dart:html`,
  ///   `dart:typed_data` can be used in dart2wasm, and since we do not want
  ///   typed lists to be considered interoperable there, it makes sense to
  ///   exclude them here.
  ///
  /// If [type] is allowed and is an extension type, it is an interop extension
  /// type as well.
  ///
  /// Returns `null` if there is no [type] that neither wraps nor is a "core"
  /// interop type.
  Reference? getCoreInteropType(DartType type) {
    if (type is ExtensionType) {
      final declaration = type.extensionTypeDeclaration;
      final reference = declaration.reference;
      if (_coreInteropTypeIndex.containsKey(reference)) {
        return _coreInteropTypeIndex[reference];
      }
      if (isJSType(declaration)) {
        return _coreInteropTypeIndex[reference] = reference;
      }
      // Note that we recurse instead of using the erasure, as JS types are
      // extension types.
      return _coreInteropTypeIndex[reference] = getCoreInteropType(
        declaration.declaredRepresentationType,
      );
    } else if (type is InterfaceType) {
      final cls = type.classNode;
      final reference = cls.reference;
      if (hasStaticInteropAnnotation(cls) ||
          (_javaScriptObject != null &&
              hasNativeAnnotation(cls) &&
              _typeEnvironment.isSubtypeOf(
                type,
                InterfaceType(_javaScriptObject, Nullability.nullable),
                SubtypeCheckMode.withNullabilities,
              ))) {
        return _coreInteropTypeIndex[reference] = reference;
      }
    }
    return null;
  }

  bool isAllowedRepresentationType(DartType type) =>
      getCoreInteropType(type) != null;

  /// If unprocessed, for all extension type members in [library] whose
  /// extension type is static interop, does the following:
  ///
  /// - Maps the extension type to its interop type
  /// - Maps the member to its extension type in `_extensionTypeIndex`.
  /// - Maps the member to its descriptor in `_extensionTypeMemberIndex`.
  /// - Maps the tear-off member to the member it tears off in
  /// `_extensionTearOffIndex`.
  void _indexExtensionTypes(Library library) {
    if (_processedExtensionTypeLibraries.contains(library)) return;
    for (var extensionType in library.extensionTypeDeclarations) {
      if (isInteropExtensionType(extensionType)) {
        for (var descriptor in extensionType.memberDescriptors) {
          final reference = descriptor.memberReference!;
          _extensionTypeMemberIndex[reference] = descriptor;
          _extensionTypeIndex[reference] = extensionType;
          final tearOffReference = descriptor.tearOffReference;
          if (tearOffReference != null) {
            _extensionTypeMemberIndex[tearOffReference] = descriptor;
            _extensionTypeIndex[tearOffReference] = extensionType;
            _extensionTypeTearOffIndex[tearOffReference] = reference;
          }
        }
      }
    }
    _processedExtensionTypeLibraries.add(library);
  }

  ExtensionTypeMemberDescriptor? getExtensionTypeDescriptor(Member member) {
    if (!member.isExtensionTypeMember) return null;
    _indexExtensionTypes(member.enclosingLibrary);
    return _extensionTypeMemberIndex[member.reference];
  }

  ExtensionTypeDeclaration? getExtensionType(Member member) {
    if (!member.isExtensionTypeMember) return null;
    _indexExtensionTypes(member.enclosingLibrary);
    return _extensionTypeIndex[member.reference];
  }

  Reference? getExtensionTypeMemberForTearOff(Member member) {
    if (!member.isExtensionTypeMember) return null;
    _indexExtensionTypes(member.enclosingLibrary);
    return _extensionTypeTearOffIndex[member.reference];
  }

  /// Return whether [node] is either an extension member that's declared as
  /// non-`static` or an extension type member that's declared as non-`static`
  /// and is not a factory or constructor.
  bool isInstanceInteropMember(Member node) {
    if (node.isExtensionMember) {
      var descriptor = getExtensionDescriptor(node);
      return descriptor != null && !descriptor.isStatic;
    } else if (node.isExtensionTypeMember) {
      var descriptor = getExtensionTypeDescriptor(node);
      return descriptor != null &&
          !descriptor.isStatic &&
          descriptor.kind != ExtensionTypeMemberKind.Constructor &&
          descriptor.kind != ExtensionTypeMemberKind.Factory;
    }
    return false;
  }

  bool _isOneOfKinds(
    Procedure node,
    ExtensionTypeMemberKind extensionTypeKind,
    ExtensionMemberKind extensionKind,
    ProcedureKind procedureKind,
  ) {
    if (node.isExtensionTypeMember) {
      return getExtensionTypeDescriptor(node)?.kind == extensionTypeKind;
    } else if (node.isExtensionMember) {
      return getExtensionDescriptor(node)?.kind == extensionKind;
    } else {
      return node.kind == procedureKind;
    }
  }

  bool isGetter(Procedure node) => _isOneOfKinds(
    node,
    ExtensionTypeMemberKind.Getter,
    ExtensionMemberKind.Getter,
    ProcedureKind.Getter,
  );

  bool isSetter(Procedure node) => _isOneOfKinds(
    node,
    ExtensionTypeMemberKind.Setter,
    ExtensionMemberKind.Setter,
    ProcedureKind.Setter,
  );

  bool isMethod(Procedure node) => _isOneOfKinds(
    node,
    ExtensionTypeMemberKind.Method,
    ExtensionMemberKind.Method,
    ProcedureKind.Method,
  );

  bool isOperator(Procedure node) => _isOneOfKinds(
    node,
    ExtensionTypeMemberKind.Operator,
    ExtensionMemberKind.Operator,
    ProcedureKind.Operator,
  );

  /// Given an interop extension type or extension member [node], gets the
  /// function type as written.
  ///
  /// Extension type and extension instance members include the instance as the
  /// first positional parameter of the generated function. Since this was never
  /// written by the user, it is excluded in the resulting function type.
  ///
  /// If not an interop extension type or extension member, returns null.
  FunctionType? getFunctionType(Procedure node) {
    if (getExtensionDescriptor(node) == null &&
        getExtensionTypeDescriptor(node) == null) {
      return null;
    }

    final functionType =
        node.signatureType ??
        node.function.computeFunctionType(Nullability.nonNullable);
    var positionalParameters = functionType.positionalParameters;
    if (isInstanceInteropMember(node)) {
      // Ignore the instance parameter.
      positionalParameters = positionalParameters.skip(1).toList();
    }
    return FunctionType(
      positionalParameters,
      functionType.returnType,
      functionType.declaredNullability,
      namedParameters: functionType.namedParameters,
      typeParameters: functionType.typeParameters,
      requiredParameterCount: functionType.requiredParameterCount,
    );
  }

  /// Return whether [node] is an external static interop constructor/factory.
  ///
  /// If [literal] is true, we check if [node] is an object literal constructor,
  /// and if not, we check that it's a non-literal constructor.
  bool _isStaticInteropConstructor(Procedure node, {required bool literal}) {
    if (!node.isExternal) return false;
    if (node.isExtensionTypeMember) {
      final kind = getExtensionTypeDescriptor(node)?.kind;
      final namedParams = node.function.namedParameters;
      return (kind == ExtensionTypeMemberKind.Constructor ||
              kind == ExtensionTypeMemberKind.Factory) &&
          (literal ? namedParams.isNotEmpty : namedParams.isEmpty);
    } else if (node.kind == ProcedureKind.Factory &&
        node.enclosingClass != null &&
        hasJSInteropAnnotation(node.enclosingClass!)) {
      final isAnonymous = hasAnonymousAnnotation(node.enclosingClass!);
      return literal ? isAnonymous : !isAnonymous;
    }
    return false;
  }

  bool isLiteralConstructor(Procedure node) =>
      _isStaticInteropConstructor(node, literal: true);

  bool isNonLiteralConstructor(Procedure node) =>
      _isStaticInteropConstructor(node, literal: false);

  bool isStaticInteropType(DartType type) {
    if (type is InterfaceType) {
      return hasStaticInteropAnnotation(type.classNode);
    } else if (type is ExtensionType) {
      return isInteropExtensionType(type.extensionTypeDeclaration);
    } else if (type is TypeParameterType || type is StructuralParameterType) {
      return isStaticInteropType(type.nonTypeParameterBound);
    }
    return false;
  }

  bool isJSType(ExtensionTypeDeclaration decl) =>
      decl.enclosingLibrary.importUri.toString() == 'dart:js_interop' &&
      decl.name.startsWith('JS');

  bool isExternalDartReference(ExtensionTypeDeclaration decl) =>
      decl.enclosingLibrary.importUri.toString() == 'dart:js_interop' &&
      decl.name == 'ExternalDartReference';

  bool isExternalDartReferenceType(DartType type) {
    if (type is ExtensionType) {
      final decl = type.extensionTypeDeclaration;
      if (_externalDartReferences.contains(decl.reference)) return true;
      if (isExternalDartReference(decl) ||
          isExternalDartReferenceType(decl.declaredRepresentationType)) {
        _externalDartReferences.add(decl.reference);
        return true;
      }
    } else if (type is TypeParameterType || type is StructuralParameterType) {
      return isExternalDartReferenceType(type.nonTypeParameterBound);
    }
    return false;
  }
}
